Skip to content

Conversation

@chrisbobbe
Copy link
Collaborator

Fixes #1936.

@chrisbobbe chrisbobbe added the maintainer review PR ready for review by Zulip maintainers label Nov 13, 2025
Copy link
Member

@rajveermalviya rajveermalviya left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @chrisbobbe! LGTM, just one small comment.

Comment on lines 29 to 31
--data-urlencode 'narrow=[{"operator":"sender", "operand":2187},
--data-urlencode 'narrow=[{"operator":"sender", "operand":13313},
{"operator":"stream", "operand":"test here"},
{"operator":"topic", "operand":"content"}]' \
{"operator":"topic", "operand":"Thumbnails"}]' \
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left from debug?

@rajveermalviya rajveermalviya added integration review Added by maintainers when PR may be ready for integration and removed maintainer review PR ready for review by Zulip maintainers labels Nov 17, 2025
This might've been done more simply by just adding the boolean value
as a new field like `thumbnailAnimated` on ImagePreviewNode.

But this value works closely with some structured data in thumbnail
URL paths, here in image previews and also in the upcoming "inline
images" feature, zulip#1913. So this new ImageThumbnailLocator class
seemed like a helpful way to present the data to consumers.
Copy link
Member

@gnprice gnprice left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for taking care of this! Comments below.

Comment on lines +557 to +559
/// Use [ImageThumbnailLocatorExtension.resolve] to obtain a suitable URL
/// for the current UI need.
/// It may not work without adding authentication credentials to the request.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like information for the ImageThumbnailLocator doc.

}

@override
int get hashCode => Object.hash(urlPath, hasAnimatedVersion);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In hashCode, seed the hash with the class name so that the hash doesn't collide with other objects that happen to also consist of a string and a bool:

Suggested change
int get hashCode => Object.hash(urlPath, hasAnimatedVersion);
int get hashCode => Object.hash('ImageThumbnailLocator', urlPath, hasAnimatedVersion);

Comment on lines +738 to +741
thumbnail: ImageThumbnailLocator(
urlPath: '/user_uploads/thumbnail/2/ce/nvoNL2LaZOciwGZ-FYagddtK/image.jpg/840x560.webp',
hasAnimatedVersion: false,
),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can keep these a bit more compact:

Suggested change
thumbnail: ImageThumbnailLocator(
urlPath: '/user_uploads/thumbnail/2/ce/nvoNL2LaZOciwGZ-FYagddtK/image.jpg/840x560.webp',
hasAnimatedVersion: false,
),
thumbnail: ImageThumbnailLocator(hasAnimatedVersion: false,
urlPath: '/user_uploads/thumbnail/2/ce/nvoNL2LaZOciwGZ-FYagddtK/image.jpg/840x560.webp'),

Comment on lines +758 to +759
urlPath: '/user_uploads/thumbnail/2/9f/tZ9c5ZmsI_cSDZ6ZdJmW8pt4/2c8d985d.gif/840x560-anim.webp',
hasAnimatedVersion: true,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. From this URL path, it sounds like the given path is the animated version. That makes the name "has animated version" seem a bit misleading.

The parser shows the attribute name in the HTML is data-animated. The most direct translation of that would be to call this field just animated. How about we use that name?

Comment on lines -562 to +564
final String? thumbnailUrl;
final ImageThumbnailLocator? thumbnail;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it'd be helpful to split this commit:

  • An NFC prep commit makes the change on this line, and the other changes that makes necessary, with the new class having only urlPath.
  • Then a separate commit adds hasAnimatedVersion (or animated per another comment).

There are a lot of places that change in boring ways just from moving this URL path out to the new class, so it'd make the substantive changes easier to read if they were in a separate commit from that.

Comment on lines +172 to +182
final store = PerAccountStoreWidget.of(context);
ThumbnailFormat? bestCandidate;

final animateIfSupported = animationMode.resolve(context);
if (hasAnimatedVersion && animateIfSupported) {
bestCandidate ??= _bestFormatOf(store.sortedAnimatedThumbnailFormats,
width: widthPhysicalPx, height: heightPhysicalPx);
}

bestCandidate ??= _bestFormatOf(store.sortedStillThumbnailFormats,
width: widthPhysicalPx, height: heightPhysicalPx);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: join up the bestCandidate computation as one stanza (and separate it from getting store)

Comment on lines +184 to +189
if (bestCandidate == null) {
// Odd if we'd need to fall back to the format encoded in [locator]'s path.
// Seems theoretically possible though:
// maybe this format isn't used now, for new uploads,
// but it was used in the past, including for this image.
return store.realmUrl.replace(path: urlPath);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is normal if there are no thumbnail formats at all, right?

In fact I think if this happens, it must be the case that there are no still thumbnail formats.

Comment on lines +189 to +192
return store.realmUrl.replace(path: urlPath);
}

final lastSlashIndex = urlPath.lastIndexOf('/');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These lines both assume that the URL string in urlPath is indeed just a path, and doesn't also contain a query and/or fragment.

I think it'd be reasonable for the API to guarantee that, but it doesn't appear to now (at https://zulip.com/api/message-formatting#images ). Without such a guarantee, a query component (though maybe not a fragment) would be a reasonable choice for this bit of API in isolation — e.g. Gravatar does that, as seen in our GravatarUrl.

(OTOH we do know it starts with a path component, because it starts with /u.)

So it'd be good to discuss that in #api documentation or #api design. Also to have the ImageThumbnailLocator constructor assert it, like it already does for the start of the path. A suitable check would be that there is no ? or # character.

'https://chat.example/user_uploads/thumbnail/1/2/a/pic.jpg/1000x2000.png');
});

testWidgets('animated version does not exist', (tester) async {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: probably clearest to put this test before its sibling — this seems like the simpler case to think about, and covers most of the interesting logic (and is surely the most common case)

Then the other test can focus just on what changes when hasAnimatedVersion is true.

Comment on lines +148 to +151
doCheck(tester, 250, 450, false, hasAnimatedVersion: false,
'https://chat.example/user_uploads/thumbnail/1/2/a/pic.jpg/500x900.jpg');
doCheck(tester, 300, 250, true, hasAnimatedVersion: false,
'https://chat.example/user_uploads/thumbnail/1/2/a/pic.jpg/840x560.webp');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, why do these get different results? Both formats should be big enough for both these desired sizes, right?

… Ah, I see: it's because they're in physical pixels.

Would be good to have a comment highlighting that. 🙂 That's also one of the key points this test is making, so it's good to make it explicit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

integration review Added by maintainers when PR may be ready for integration

Projects

None yet

Development

Successfully merging this pull request may close these issues.

content: Disable image-preview animations, subject to device settings

3 participants